package com.beerandjesus.sportofkings.handicapper.dao.aspect;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.apache.log4j.Logger;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.TransactionStatus;
import com.beerandjesus.sportofkings.handicapper.dao.Database;

import java.util.Map;
import java.util.List;
import java.util.ArrayList;

/**
 * Aspect for management of Spring configured TransactionTemplate transactions.
 * <p/>
 * This object MUST be provided with a map associating each Database.Datasource with its
 * associated TransactionTemplate reference.
 */
@Aspect
public class SpringTransactionAspect extends AbstractAnnotationAspect<TransactionBoundary> {

    private static final Logger LOG = Logger.getLogger(SpringTransactionAspect.class);

    public Logger getLogger() {
        return LOG;
    }

    private Map<String, TransactionTemplate> transactionTemplates;

    /**
     * Apply transactional semantics to a method. The method
     * must be annotated with @TransactionBoundary specifying
     * the Database.Datasource that needs to be used.
     *
     * @param pjp The join point
     * @return The method's return object
     * @throws Throwable When something goes wrong
     */
    public Object doInTransaction(final ProceedingJoinPoint pjp) throws Throwable {
        final TransactionBoundary anno = obtainAnnotation(pjp, TransactionBoundary.class);
        if (anno == null) {
            throw new RuntimeException("TransactionBoundary annotation not found");
        }
        Database.Datasource[] dss = anno.datasources();
        final List<TransactionTemplate> list = new ArrayList<TransactionTemplate>(2);
        for (Database.Datasource d : dss) {
            String dsName = d.toString();
            TransactionTemplate template = transactionTemplates.get(dsName);
            if (template == null) {
                throw new RuntimeException("TransactionTemplate requested by name " + dsName + " does not exist");
            }
            list.add(template);
        }
        final Throwable[] t = new Throwable[1];
        Object toReturn;
        if (list.size() == 1) {
            toReturn = list.get(0).execute(new TransactionCallback() {
                public Object doInTransaction(TransactionStatus transactionStatus) {
                    try {
                        return pjp.proceed();
                    } catch (Throwable throwable) {
                        t[0] = throwable;
                        return null;
                    }
                }
            });
        } else if (list.size() == 2) {
            toReturn = list.get(0).execute(new TransactionCallback() {
                public Object doInTransaction(TransactionStatus transactionStatus) {
                    return list.get(1).execute(new TransactionCallback() {
                        public Object doInTransaction(TransactionStatus transactionStatus) {
                            try {
                                return pjp.proceed();
                            } catch (Throwable throwable) {
                                t[0] = throwable;
                                return null;
                            }
                        }
                    });
                }
            });
        } else {
            throw new RuntimeException("Unexpected number of transaction templates found: " + list.size());
        }
        if (t[0] != null) {
            if (anno.expectedExceptions() != null) {
                for (Class<? extends Throwable> throwable: anno.expectedExceptions()) {
                    if (t[0].getClass().isAssignableFrom(throwable)) {
                        throw t[0];
                    }
                }
            }
            throw new RuntimeException("Error thrown in transaction", t[0]);
        }
        return toReturn;
    }

    public void setTransactionTemplates(Map<String, TransactionTemplate> transactionTemplates) {
        this.transactionTemplates = transactionTemplates;
    }
}
